/**
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF licenses this file
 * to you under the Apache License, Version 2.0 (the
 * "License"); you may not use this file except in compliance
 * with the License.  You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package org.apache.hadoop.hbase.util;

import java.io.PrintWriter;
import java.io.StringWriter;
import java.net.InetAddress;
import java.net.URI;
import java.net.URISyntaxException;
import java.net.UnknownHostException;
import java.text.DateFormat;
import java.text.DecimalFormat;
import java.text.NumberFormat;
import java.util.Locale;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import java.util.StringTokenizer;
import java.util.Collection;

import org.apache.hadoop.hbase.fs.*;

/**
 * General string utils
 */
public class StringUtils {

	private static final DecimalFormat decimalFormat;
	static {
		NumberFormat numberFormat = NumberFormat.getNumberInstance(Locale.ENGLISH);
		decimalFormat = (DecimalFormat) numberFormat;
		decimalFormat.applyPattern("#.##");
	}

	/**
	 * Make a string representation of the exception.
	 * @param e The exception to stringify
	 * @return A string with exception name and call stack.
	 */
	public static String stringifyException(Throwable e) {
		StringWriter stm = new StringWriter();
		PrintWriter wrt = new PrintWriter(stm);
		e.printStackTrace(wrt);
		wrt.close();
		return stm.toString();
	}

	/**
	 * Given a full hostname, return the word upto the first dot.
	 * @param fullHostname the full hostname
	 * @return the hostname to the first dot
	 */
	public static String simpleHostname(String fullHostname) {
		int offset = fullHostname.indexOf('.');
		if (offset != -1) {
			return fullHostname.substring(0, offset);
		}
		return fullHostname;
	}

	private static DecimalFormat oneDecimal = new DecimalFormat("0.0");

	/**
	 * Given an integer, return a string that is in an approximate, but human 
	 * readable format. 
	 * It uses the bases 'k', 'm', and 'g' for 1024, 1024**2, and 1024**3.
	 * @param number the number to format
	 * @return a human readable form of the integer
	 */
	public static String humanReadableInt(long number) {
		long absNumber = Math.abs(number);
		double result = number;
		String suffix = "";
		if (absNumber < 1024) {
			// nothing
		} else if (absNumber < 1024 * 1024) {
			result = number / 1024.0;
			suffix = "k";
		} else if (absNumber < 1024 * 1024 * 1024) {
			result = number / (1024.0 * 1024);
			suffix = "m";
		} else {
			result = number / (1024.0 * 1024 * 1024);
			suffix = "g";
		}
		return oneDecimal.format(result) + suffix;
	}

	/**
	 * Format a percentage for presentation to the user.
	 * @param done the percentage to format (0.0 to 1.0)
	 * @param digits the number of digits past the decimal point
	 * @return a string representation of the percentage
	 */
	public static String formatPercent(double done, int digits) {
		DecimalFormat percentFormat = new DecimalFormat("0.00%");
		double scale = Math.pow(10.0, digits + 2);
		double rounded = Math.floor(done * scale);
		percentFormat.setDecimalSeparatorAlwaysShown(false);
		percentFormat.setMinimumFractionDigits(digits);
		percentFormat.setMaximumFractionDigits(digits);
		return percentFormat.format(rounded / scale);
	}

	/**
	 * Given an array of strings, return a comma-separated list of its elements.
	 * @param strs Array of strings
	 * @return Empty string if strs.length is 0, comma separated list of strings
	 * otherwise
	 */

	public static String arrayToString(String[] strs) {
		if (strs.length == 0) {
			return "";
		}
		StringBuffer sbuf = new StringBuffer();
		sbuf.append(strs[0]);
		for (int idx = 1; idx < strs.length; idx++) {
			sbuf.append(",");
			sbuf.append(strs[idx]);
		}
		return sbuf.toString();
	}

	/**
	 * Given an array of bytes it will convert the bytes to a hex string
	 * representation of the bytes
	 * @param bytes
	 * @param start start index, inclusively
	 * @param end end index, exclusively
	 * @return hex string representation of the byte array
	 */
	public static String byteToHexString(byte[] bytes, int start, int end) {
		if (bytes == null) {
			throw new IllegalArgumentException("bytes == null");
		}
		StringBuilder s = new StringBuilder();
		for (int i = start; i < end; i++) {
			s.append(String.format("%02x", bytes[i]));
		}
		return s.toString();
	}

	/** Same as byteToHexString(bytes, 0, bytes.length). */
	public static String byteToHexString(byte bytes[]) {
		return byteToHexString(bytes, 0, bytes.length);
	}

	/**
	 * Given a hexstring this will return the byte array corresponding to the
	 * string
	 * @param hex the hex String array
	 * @return a byte array that is a hex string representation of the given
	 *         string. The size of the byte array is therefore hex.length/2
	 */
	public static byte[] hexStringToByte(String hex) {
		byte[] bts = new byte[hex.length() / 2];
		for (int i = 0; i < bts.length; i++) {
			bts[i] = (byte) Integer.parseInt(hex.substring(2 * i, 2 * i + 2), 16);
		}
		return bts;
	}

	/**
	 * 
	 * @param uris
	 */
	public static String uriToString(URI[] uris) {
		if (uris == null) {
			return null;
		}
		StringBuffer ret = new StringBuffer(uris[0].toString());
		for (int i = 1; i < uris.length; i++) {
			ret.append(",");
			ret.append(uris[i].toString());
		}
		return ret.toString();
	}

	/**
	 * 
	 * @param str
	 */
	public static URI[] stringToURI(String[] str) {
		if (str == null)
			return null;
		URI[] uris = new URI[str.length];
		for (int i = 0; i < str.length; i++) {
			try {
				uris[i] = new URI(str[i]);
			} catch (URISyntaxException ur) {
				System.out.println("Exception in specified URI's " + StringUtils.stringifyException(ur));
				//making sure its asssigned to null in case of an error
				uris[i] = null;
			}
		}
		return uris;
	}

	/**
	 * 
	 * @param str
	 */
	public static Path[] stringToPath(String[] str) {
		if (str == null) {
			return null;
		}
		Path[] p = new Path[str.length];
		for (int i = 0; i < str.length; i++) {
			p[i] = new Path(str[i]);
		}
		return p;
	}

	/**
	 * 
	 * Given a finish and start time in long milliseconds, returns a 
	 * String in the format Xhrs, Ymins, Z sec, for the time difference between two times. 
	 * If finish time comes before start time then negative valeus of X, Y and Z wil return. 
	 * 
	 * @param finishTime finish time
	 * @param startTime start time
	 */
	public static String formatTimeDiff(long finishTime, long startTime) {
		long timeDiff = finishTime - startTime;
		return formatTime(timeDiff);
	}

	/**
	 * 
	 * Given the time in long milliseconds, returns a 
	 * String in the format Xhrs, Ymins, Z sec. 
	 * 
	 * @param timeDiff The time difference to format
	 */
	public static String formatTime(long timeDiff) {
		StringBuffer buf = new StringBuffer();
		long hours = timeDiff / (60 * 60 * 1000);
		long rem = (timeDiff % (60 * 60 * 1000));
		long minutes = rem / (60 * 1000);
		rem = rem % (60 * 1000);
		long seconds = rem / 1000;

		if (hours != 0) {
			buf.append(hours);
			buf.append("hrs, ");
		}
		if (minutes != 0) {
			buf.append(minutes);
			buf.append("mins, ");
		}
		// return "0sec if no difference
		buf.append(seconds);
		buf.append("sec");
		return buf.toString();
	}

	/**
	 * Formats time in ms and appends difference (finishTime - startTime) 
	 * as returned by formatTimeDiff().
	 * If finish time is 0, empty string is returned, if start time is 0 
	 * then difference is not appended to return value. 
	 * @param dateFormat date format to use
	 * @param finishTime fnish time
	 * @param startTime start time
	 * @return formatted value. 
	 */
	public static String getFormattedTimeWithDiff(DateFormat dateFormat, long finishTime, long startTime) {
		StringBuffer buf = new StringBuffer();
		if (0 != finishTime) {
			buf.append(dateFormat.format(new Date(finishTime)));
			if (0 != startTime) {
				buf.append(" (" + formatTimeDiff(finishTime, startTime) + ")");
			}
		}
		return buf.toString();
	}

	/**
	 * Returns an arraylist of strings.
	 * @param str the comma seperated string values
	 * @return the arraylist of the comma seperated string values
	 */
	public static String[] getStrings(String str) {
		Collection<String> values = getStringCollection(str);
		if (values.size() == 0) {
			return null;
		}
		return values.toArray(new String[values.size()]);
	}

	/**
	 * Returns a collection of strings.
	 * @param str comma seperated string values
	 * @return an <code>ArrayList</code> of string values
	 */
	public static Collection<String> getStringCollection(String str) {
		List<String> values = new ArrayList<String>();
		if (str == null)
			return values;
		StringTokenizer tokenizer = new StringTokenizer(str, ",");
		values = new ArrayList<String>();
		while (tokenizer.hasMoreTokens()) {
			values.add(tokenizer.nextToken());
		}
		return values;
	}

	final public static char COMMA = ',';
	final public static String COMMA_STR = ",";
	final public static char ESCAPE_CHAR = '\\';

	/**
	 * Split a string using the default separator
	 * @param str a string that may have escaped separator
	 * @return an array of strings
	 */
	public static String[] split(String str) {
		return split(str, ESCAPE_CHAR, COMMA);
	}

	/**
	 * Split a string using the given separator
	 * @param str a string that may have escaped separator
	 * @param escapeChar a char that be used to escape the separator
	 * @param separator a separator char
	 * @return an array of strings
	 */
	public static String[] split(String str, char escapeChar, char separator) {
		if (str == null) {
			return null;
		}
		ArrayList<String> strList = new ArrayList<String>();
		StringBuilder split = new StringBuilder();
		int index = 0;
		while ((index = findNext(str, separator, escapeChar, index, split)) >= 0) {
			++index; // move over the separator for next search
			strList.add(split.toString());
			split.setLength(0); // reset the buffer 
		}
		strList.add(split.toString());
		// remove trailing empty split(s)
		int last = strList.size(); // last split
		while (--last >= 0 && "".equals(strList.get(last))) {
			strList.remove(last);
		}
		return strList.toArray(new String[strList.size()]);
	}

	/**
	 * Finds the first occurrence of the separator character ignoring the escaped
	 * separators starting from the index. Note the substring between the index
	 * and the position of the separator is passed.
	 * @param str the source string
	 * @param separator the character to find
	 * @param escapeChar character used to escape
	 * @param start from where to search
	 * @param split used to pass back the extracted string
	 */
	public static int findNext(String str, char separator, char escapeChar, int start, StringBuilder split) {
		int numPreEscapes = 0;
		for (int i = start; i < str.length(); i++) {
			char curChar = str.charAt(i);
			if (numPreEscapes == 0 && curChar == separator) { // separator 
				return i;
			} else {
				split.append(curChar);
				numPreEscapes = (curChar == escapeChar) ? (++numPreEscapes) % 2 : 0;
			}
		}
		return -1;
	}

	/**
	 * Escape commas in the string using the default escape char
	 * @param str a string
	 * @return an escaped string
	 */
	public static String escapeString(String str) {
		return escapeString(str, ESCAPE_CHAR, COMMA);
	}

	/**
	 * Escape <code>charToEscape</code> in the string 
	 * with the escape char <code>escapeChar</code>
	 * 
	 * @param str string
	 * @param escapeChar escape char
	 * @param charToEscape the char to be escaped
	 * @return an escaped string
	 */
	public static String escapeString(String str, char escapeChar, char charToEscape) {
		return escapeString(str, escapeChar, new char[] { charToEscape });
	}

	// check if the character array has the character 
	private static boolean hasChar(char[] chars, char character) {
		for (char target : chars) {
			if (character == target) {
				return true;
			}
		}
		return false;
	}

	/**
	 * @param charsToEscape array of characters to be escaped
	 */
	public static String escapeString(String str, char escapeChar, char[] charsToEscape) {
		if (str == null) {
			return null;
		}
		int len = str.length();
		// Let us specify good enough capacity to constructor of StringBuilder sothat
		// resizing would not be needed(to improve perf).
		StringBuilder result = new StringBuilder((int) (len * 1.5));

		for (int i = 0; i < len; i++) {
			char curChar = str.charAt(i);
			if (curChar == escapeChar || hasChar(charsToEscape, curChar)) {
				// special char
				result.append(escapeChar);
			}
			result.append(curChar);
		}
		return result.toString();
	}

	/**
	 * Unescape commas in the string using the default escape char
	 * @param str a string
	 * @return an unescaped string
	 */
	public static String unEscapeString(String str) {
		return unEscapeString(str, ESCAPE_CHAR, COMMA);
	}

	/**
	 * Unescape <code>charToEscape</code> in the string 
	 * with the escape char <code>escapeChar</code>
	 * 
	 * @param str string
	 * @param escapeChar escape char
	 * @param charToEscape the escaped char
	 * @return an unescaped string
	 */
	public static String unEscapeString(String str, char escapeChar, char charToEscape) {
		return unEscapeString(str, escapeChar, new char[] { charToEscape });
	}

	/**
	 * @param charsToEscape array of characters to unescape
	 */
	public static String unEscapeString(String str, char escapeChar, char[] charsToEscape) {
		if (str == null) {
			return null;
		}
		StringBuilder result = new StringBuilder(str.length());
		boolean hasPreEscape = false;
		for (int i = 0; i < str.length(); i++) {
			char curChar = str.charAt(i);
			if (hasPreEscape) {
				if (curChar != escapeChar && !hasChar(charsToEscape, curChar)) {
					// no special char
					throw new IllegalArgumentException("Illegal escaped string " + str + " unescaped " + escapeChar + " at "
							+ (i - 1));
				}
				// otherwise discard the escape char
				result.append(curChar);
				hasPreEscape = false;
			} else {
				if (hasChar(charsToEscape, curChar)) {
					throw new IllegalArgumentException("Illegal escaped string " + str + " unescaped " + curChar + " at " + i);
				} else if (curChar == escapeChar) {
					hasPreEscape = true;
				} else {
					result.append(curChar);
				}
			}
		}
		if (hasPreEscape) {
			throw new IllegalArgumentException("Illegal escaped string " + str + ", not expecting " + escapeChar + " in the end.");
		}
		return result.toString();
	}

	/**
	 * Return hostname without throwing exception.
	 * @return hostname
	 */
	public static String getHostname() {
		try {
			return "" + InetAddress.getLocalHost();
		} catch (UnknownHostException uhe) {
			return "" + uhe;
		}
	}

	/**
	 * The traditional binary prefixes, kilo, mega, ..., exa,
	 * which can be represented by a 64-bit integer.
	 * TraditionalBinaryPrefix symbol are case insensitive. 
	 */
	public static enum TraditionalBinaryPrefix {
		KILO(1024), MEGA(KILO.value << 10), GIGA(MEGA.value << 10), TERA(GIGA.value << 10), PETA(TERA.value << 10), EXA(
				PETA.value << 10);

		public final long value;
		public final char symbol;

		TraditionalBinaryPrefix(long value) {
			this.value = value;
			this.symbol = toString().charAt(0);
		}

		/**
		 * @return The TraditionalBinaryPrefix object corresponding to the symbol.
		 */
		public static TraditionalBinaryPrefix valueOf(char symbol) {
			symbol = Character.toUpperCase(symbol);
			for (TraditionalBinaryPrefix prefix : TraditionalBinaryPrefix.values()) {
				if (symbol == prefix.symbol) {
					return prefix;
				}
			}
			throw new IllegalArgumentException("Unknown symbol '" + symbol + "'");
		}

		/**
		 * Convert a string to long.
		 * The input string is first be trimmed
		 * and then it is parsed with traditional binary prefix.
		 *
		 * For example,
		 * "-1230k" will be converted to -1230 * 1024 = -1259520;
		 * "891g" will be converted to 891 * 1024^3 = 956703965184;
		 *
		 * @param s input string
		 * @return a long value represented by the input string.
		 */
		public static long string2long(String s) {
			s = s.trim();
			final int lastpos = s.length() - 1;
			final char lastchar = s.charAt(lastpos);
			if (Character.isDigit(lastchar))
				return Long.parseLong(s);
			else {
				long prefix = TraditionalBinaryPrefix.valueOf(lastchar).value;
				long num = Long.parseLong(s.substring(0, lastpos));
				if (num > (Long.MAX_VALUE / prefix) || num < (Long.MIN_VALUE / prefix)) {
					throw new IllegalArgumentException(s + " does not fit in a Long");
				}
				return num * prefix;
			}
		}
	}

	/**
	 * Escapes HTML Special characters present in the string.
	 * @param string
	 * @return HTML Escaped String representation
	 */
	public static String escapeHTML(String string) {
		if (string == null) {
			return null;
		}
		StringBuffer sb = new StringBuffer();
		boolean lastCharacterWasSpace = false;
		char[] chars = string.toCharArray();
		for (char c : chars) {
			if (c == ' ') {
				if (lastCharacterWasSpace) {
					lastCharacterWasSpace = false;
					sb.append("&nbsp;");
				} else {
					lastCharacterWasSpace = true;
					sb.append(" ");
				}
			} else {
				lastCharacterWasSpace = false;
				switch (c) {
				case '<':
					sb.append("&lt;");
					break;
				case '>':
					sb.append("&gt;");
					break;
				case '&':
					sb.append("&amp;");
					break;
				case '"':
					sb.append("&quot;");
					break;
				default:
					sb.append(c);
					break;
				}
			}
		}

		return sb.toString();
	}

	/**
	 * Return an abbreviated English-language desc of the byte length
	 */
	public static String byteDesc(long len) {
		double val = 0.0;
		String ending = "";
		if (len < 1024 * 1024) {
			val = (1.0 * len) / 1024;
			ending = " KB";
		} else if (len < 1024 * 1024 * 1024) {
			val = (1.0 * len) / (1024 * 1024);
			ending = " MB";
		} else if (len < 1024L * 1024 * 1024 * 1024) {
			val = (1.0 * len) / (1024 * 1024 * 1024);
			ending = " GB";
		} else if (len < 1024L * 1024 * 1024 * 1024 * 1024) {
			val = (1.0 * len) / (1024L * 1024 * 1024 * 1024);
			ending = " TB";
		} else {
			val = (1.0 * len) / (1024L * 1024 * 1024 * 1024 * 1024);
			ending = " PB";
		}
		return limitDecimalTo2(val) + ending;
	}

	public static synchronized String limitDecimalTo2(double d) {
		return decimalFormat.format(d);
	}

	/**
	 * Concatenates strings, using a separator.
	 *
	 * @param separator Separator to join with.
	 * @param strings Strings to join.
	 * @return  the joined string
	 */
	public static String join(CharSequence separator, Iterable<String> strings) {
		StringBuilder sb = new StringBuilder();
		boolean first = true;
		for (String s : strings) {
			if (first) {
				first = false;
			} else {
				sb.append(separator);
			}
			sb.append(s);
		}
		return sb.toString();
	}

	/**
	 * Concatenates strings, using a separator.
	 *
	 * @param separator to join with
	 * @param strings to join
	 * @return  the joined string
	 */
	public static String join(CharSequence separator, String[] strings) {
		// Ideally we don't have to duplicate the code here if array is iterable.
		StringBuilder sb = new StringBuilder();
		boolean first = true;
		for (String s : strings) {
			if (first) {
				first = false;
			} else {
				sb.append(separator);
			}
			sb.append(s);
		}
		return sb.toString();
	}

	/**
	 * Capitalize a word
	 *
	 * @param s the input string
	 * @return capitalized string
	 */
	public static String capitalize(String s) {
		int len = s.length();
		if (len == 0)
			return s;
		return new StringBuilder(len).append(Character.toTitleCase(s.charAt(0))).append(s.substring(1)).toString();
	}

	/**
	 * Convert SOME_STUFF to SomeStuff
	 *
	 * @param s input string
	 * @return camelized string
	 */
	public static String camelize(String s) {
		StringBuilder sb = new StringBuilder();
		String[] words = split(s.toLowerCase(Locale.US), ESCAPE_CHAR, '_');

		for (String word : words)
			sb.append(capitalize(word));

		return sb.toString();
	}
}
